package com.lemon.exam.common.util;

import com.lemon.exam.common.constant.ApiConst;
import com.lemon.exam.common.exception.CustomException;
import com.lemon.exam.common.properties.ConfigProperties;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import jakarta.annotation.Resource;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 代码生成器
 *
 * @author Lemon
 * @since 2025/2/29 13:00
 */
@Slf4j
@Component
@ConditionalOnExpression("'${spring.profiles.active}'.equals('dev') ? true : false")
public final class GeneratorUtil {
    private static final String JAVA_PATH = String.format("%s%ssrc%smain%sjava%scom%slemon%sexam%s",System.getProperty("user.dir"), File.separator, File.separator, File.separator, File.separator, File.separator, File.separator, File.separator);
    private final Object lock = new Object();
    @Resource
    private DatabaseClient databaseClient;
    @Resource
    private R2dbcProperties r2dbcProperties;
    @Resource
    private ConfigProperties configProperties;

    /**
     * 生成代码
     *
     * @param tableName
     */
    public Mono<Boolean> generator(String tableName) {
        return generator("", tableName);
    }

    /**
     * 生成代码
     *
     * @param prefix
     * @param tableName
     */
    public Mono<Boolean> generator(String prefix, String tableName) {
        if (!StringUtils.hasText(tableName)) {
            return Mono.error(new CustomException("请指定要生成的表名！"));
        }
        //获取数据库信息
        String dbName = getDbName();
        return getTableInfo(tableName, prefix, dbName).flatMap(this::generator);
    }

    /**
     * 生成代码
     *
     * @param prefix
     * @param tableNames
     */
    public Flux<Boolean> generator(String prefix, String... tableNames) {
        if (tableNames == null || tableNames.length == 0) {
            return Flux.error(new CustomException("请指定要生成的表名！"));
        }
        //获取数据库信息
        String dbName = getDbName();
        return Flux.fromArray(tableNames).flatMap(tableName -> getTableInfo(tableName, prefix, dbName).flatMap(this::generator));
    }

    /**
     * 执行代码生成
     *
     * @param table
     * @return
     */
    private Mono<Boolean> generator(Table table) {
        log.info("开始生成代码...");
        return Mono.fromCallable(() -> {
            //1.获取 FreeMarker 配置信息
            Configuration configuration = getConfiguration();

            //2.生成PO对象
            String poContent = toClassContent("entity.po.java", table, configuration);
            String poPath = JAVA_PATH + "entity" + File.separator + "po" + File.separator + table.getJavaName() + "PO.java";
            writer(poPath, poContent);

            //3.生成Repository对象
            String repositoryContent = toClassContent("repository.java", table, configuration);
            String repositoryPath = JAVA_PATH + "repository" + File.separator + table.getJavaName() + "Repository.java";
            writer(repositoryPath, repositoryContent);

            //4.生成Service对象
            String serviceContent = toClassContent("service.java", table, configuration);
            String servicePath = JAVA_PATH + "service" + File.separator + "I" + table.getJavaName() + "Service.java";
            writer(servicePath, serviceContent);

            //5.生成Service实现类对象
            String serviceImplContent = toClassContent("service.impl.java", table, configuration);
            String serviceImplPath = JAVA_PATH + "service" + File.separator + "impl" + File.separator + table.getJavaName() + "ServiceImpl.java";
            writer(serviceImplPath, serviceImplContent);

            //6.生成Controller对象
            String controllerContent = toClassContent("controller.java", table, configuration);
            String controllerPath = JAVA_PATH + "controller" + File.separator + table.getJavaName() + "Controller.java";
            writer(controllerPath, controllerContent);

            //7.生成DTO对象
            String dtoContent = toClassContent("entity.dto.java", table, configuration);
            String dtoPath = JAVA_PATH + "entity" + File.separator + "dto" + File.separator + table.getJavaName() + "DTO.java";
            writer(dtoPath, dtoContent);

            //8.生成VO对象
            String voContent = toClassContent("entity.vo.java", table, configuration);
            String voPath = JAVA_PATH + "entity" + File.separator + "vo" + File.separator + table.getJavaName() + "VO.java";
            writer(voPath, voContent);

            //9.生成VUE文件
            String vueContent = toClassContent("web.vue", table, configuration);
            String vuePath = configProperties.getGenerator().getWebPath() + File.separator + table.getJavaName() + ".vue";
            writer(vuePath, vueContent);
            log.info("代码生成完毕！");
            return true;
        });
    }

    /**
     * 将模板渲染成字符串
     *
     * @param template
     * @param table
     * @param configuration
     * @return
     */
    private String toClassContent(String template, Table table, Configuration configuration) {
        try (StringWriter out = new StringWriter()) {
            Template temp = configuration.getTemplate(template + ".ftl"); // 加载模板文件
            temp.process(table, out); // 填充模板并输出到StringWriter
            return out.toString();
        } catch (IOException | TemplateException e) {
            e.printStackTrace();
            throw new CustomException("模板渲染失败！");
        }
    }

    /**
     * 写入文件
     *
     * @param path
     * @param content
     * @return
     */
    private void writer(String path, String content) {
        //1.判断文件是否可以覆盖
        if (!configProperties.getGenerator().getFileOverride() && new File(path).exists()) {
            log.error("文件已存在，请修改文件名：{}", path);
            return;
        }
        //2.判断文件夹是否存在
        String dirPath = path.substring(0, path.lastIndexOf(File.separator));
        File dirFile = new File(dirPath);
        if (!dirFile.exists()) {
            synchronized (lock) {
                if (!dirFile.exists()) {
                    log.info("文件夹不存在需要创建");
                    boolean mkdirs = dirFile.mkdirs();
                    if (!mkdirs) {
                        throw new CustomException("文件夹创建失败，请检查文件夹是否有写入权限！");
                    }
                }
            }
        }
        //3.向文件中写入内容并创建文件
        try (FileWriter writer = new FileWriter(path)) {
            writer.write(content);
        } catch (IOException e) {
            e.printStackTrace();
            throw new CustomException("文件写入失败！");
        }
    }

    /**
     * 获取配置信息
     *
     * @return
     */
    private Configuration getConfiguration() {
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_32);
        configuration.setClassForTemplateLoading(this.getClass(), "/templates");
        configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        configuration.setDefaultEncoding("UTF-8");
        configuration.setOutputEncoding("UTF-8");
        configuration.setLogTemplateExceptions(false);
        configuration.setWrapUncheckedExceptions(true);
        return configuration;
    }

    /**
     * 获取表信息
     *
     * @param tableName
     * @param prefix
     * @return
     */
    private Mono<Table> getTableInfo(String tableName, String prefix, String dbName) {
        return getColumns(tableName, dbName).collectList().flatMap(columns -> getTable(tableName, prefix, dbName).map(table -> {
            table.setColumns(columns);
            List<Column> primaryKey = columns.stream().filter(Column::getIsPrimaryKey).toList();
            if (primaryKey.size() > 0) {
                table.setPrimaryKeyType(primaryKey.get(0).getJavaType());
            } else {
                table.setPrimaryKeyType("String");
            }
            return table;
        }));
    }

    /**
     * 查询列信息
     *
     * @param tableName
     * @return
     */
    private static final String GET_COLUMN_SQL = """
            SELECT COLUMN_NAME,COLUMN_KEY,DATA_TYPE,COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS
            WHERE TABLE_SCHEMA = :tableSchema AND TABLE_NAME = :tableName ORDER BY ORDINAL_POSITION
            """;
    private Flux<Column> getColumns(String tableName, String tableSchema) {
        log.info("正在查询列信息...");
        return databaseClient.sql(GET_COLUMN_SQL)
                .bind("tableSchema", tableSchema)
                .bind("tableName", tableName)
                .fetch()
                .all()
                .map(row -> {
                    Column column = new Column();
                    Object columnName = row.get("COLUMN_NAME");
                    if (Objects.nonNull(columnName)) {
                        column.setColumnName(columnName.toString());
                        column.setJavaName(toLowerCamelCase(columnName.toString()));
                    }
                    Object dataType = row.get("DATA_TYPE");
                    if (Objects.nonNull(dataType)) {
                        column.setDataType(dataType.toString());
                        column.setJavaType(typeToJavaType(dataType.toString()));
                    }
                    Object columnComment = row.get("COLUMN_COMMENT");
                    if (Objects.nonNull(columnComment)) {
                        column.setColumnComment(columnComment.toString());
                    }
                    Object isPrimaryKey = row.get("COLUMN_KEY");
                    if (Objects.nonNull(isPrimaryKey)) {
                        column.setIsPrimaryKey("PRI".equals(isPrimaryKey.toString()));
                    }
                    return column;
                });
    }

    /**
     * 获取表信息
     *
     * @param tableName
     * @param prefix
     * @return
     */
    private static final String GET_TABLE_SQL = """
            SELECT TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES
            WHERE TABLE_SCHEMA = :tableSchema AND TABLE_NAME = :tableName
            """;
    private Mono<Table> getTable(String tableName, String prefix, String tableSchema) {
        log.info("正在查询表信息...");
        return databaseClient.sql(GET_TABLE_SQL)
                .bind("tableSchema", tableSchema)
                .bind("tableName", tableName)
                .fetch()
                .one()
                .map(row -> {
                    Table table = new Table();
                    table.setTableName(tableName);
                    Object tableComment = row.get("TABLE_COMMENT");
                    if (Objects.nonNull(tableComment)) {
                        table.setTableComment(tableComment.toString());
                    }
                    String javaName = getJavaName(tableName, prefix);
                    table.setJavaName(javaName);
                    table.setTime(LocalDateTime.now().format(ApiConst.DATE_FORMATTER_11));
                    table.setPath(javaName.substring(0, 1).toLowerCase() + javaName.substring(1));
                    table.setAuthor(configProperties.getGenerator().getAuthor());
                    return table;
                });
    }

    /**
     * 获取数据库名称
     *
     * @return
     */
    private String getDbName() {
        String url = r2dbcProperties.getUrl();
        if (StringUtils.hasText(url) && url.startsWith("r2dbc:mysql://")) {
            String[] split = url.replace("r2dbc:mysql://", "").split("/");
            String databasePart = split[1];
            // 去除查询参数
            if (databasePart.contains("?")) {
                return databasePart.split("\\?")[0];
            }
            return databasePart;
        }
        throw new CustomException("数据库连接信息错误！");
    }

    /**
     * 转换大驼峰命名
     *
     * @param tableName
     * @param prefix
     * @return
     */
    private static String getJavaName(String tableName, String prefix) {
        if (StringUtils.hasText(prefix) && tableName.startsWith(prefix)) {
            String name = toLowerCamelCase(tableName.substring(prefix.length()));
            return name.substring(0, 1).toUpperCase() + name.substring(1);
        } else {
            String name = toLowerCamelCase(tableName);
            return name.substring(0, 1).toUpperCase() + name.substring(1);
        }
    }

    /**
     * 将字符串转换为驼峰命名
     *
     * @param str
     * @return
     */
    private static String toLowerCamelCase(String str) {
        if (!StringUtils.hasText(str)) {
            return str;
        }
        String[] split = str.split("_");
        StringBuilder sb = new StringBuilder(split[0].substring(0, 1).toLowerCase() + split[0].substring(1));
        if (split.length > 1) {
            for (int i = 1; i < split.length; i++) {
                sb.append(split[i].substring(0, 1).toUpperCase()).append(split[i].substring(1));
            }
        }
        return sb.toString();
    }

    /**
     * 将数据库类型转换为java类型
     *
     * @param type
     * @return
     */
    private static String typeToJavaType(String type) {
        return switch (type) {
            case "varchar", "char", "tinytext", "text", "mediumtext", "longtext" -> "String";
            case "int", "tinyint", "smallint", "mediumint" -> "Integer";
            case "bigint" -> "Long";
            case "float", "double" -> "Double";
            case "decimal" -> "BigDecimal";
            case "date" -> "LocalDate";
            case "datetime", "timestamp" -> "LocalDateTime";
            case "time" -> "LocalTime";
            default -> throw new CustomException("数据库类型错误: " + type);
        };
    }

    @Getter
    @Setter
    public static class Table {
        //表名
        private String tableName;
        //java类名
        private String javaName;
        //表注释
        private String tableComment;
        //主键类型
        private String primaryKeyType;
        //当前时间
        private String time;
        //路径
        private String path;
        //作者
        private String author;
        //列信息
        private List<Column> columns;
    }

    @Getter
    @Setter
    public static class Column {
        //列名
        private String columnName;
        //数据类型
        private String dataType;
        //是否是主键
        private Boolean isPrimaryKey;
        //注释
        private String columnComment;
        //java属性名
        private String javaName;
        //java属性类型
        private String javaType;
    }
}
